/*
 * 日志落地模块
 *
 * 将格式化后的日志字符串输出到指定位置，
 * 支持标准输出、文件、滚动文件，支持拓展落地方向
 * 使用工厂模式管理所有落地类的创建
 *
 **/

#pragma once

#include <fstream>
#include <string>
#include <list>
#include <memory>
#include <mutex>
#include <unordered_map>
// #ifdef __linux__
// #   include <ext/stdio_filebuf.h>
// #endif
#include "config.hpp"
#include "message.hpp"
#include "format.hpp"
#include "util.hpp"
#include "except.hpp"

namespace easylog
{
    class sinker
    {
    public:
        using ptr = std::shared_ptr<sinker>;

    public:
        sinker(formatter::ptr fmtr = std::make_shared<formatter>())
            : _fmtr(fmtr)
        {}

        void set_pattern(const std::string& pattern)
        {
            _fmtr->set_pattern(pattern);
        }

        void set_formatter(formatter::ptr fmtr)
        {
            _fmtr = fmtr;
        }

        // interface all sinkers used
        virtual ~sinker() {}
        virtual void sink(message&& msg) = 0;
        virtual void flush() = 0;

        // interfaces only used by stdout sinker
        virtual void set_color(bool onclr) = 0;
        virtual void set_color_mode(color_mode mode) = 0;

        virtual void set_logfile(const std::string& path) = 0;
        virtual void set_fn_patt(const std::string& patt) = 0;

        virtual void set_order(size_t n) = 0;
        virtual void set_max_history(size_t n) = 0;
        virtual void set_max_file_size(size_enum size) = 0;
        virtual void set_max_live_time(time_enum time) = 0;
        virtual void set_total_size_cap(size_enum size) = 0;

    protected:
        // Create the logfile and the directory,
        // and use the parameters ofs to reopen it.
        void open_logfile(std::ofstream* ofs, std::string path)
        {
            util::file::mkdir(util::file::dirpath(path));
            util::file::mkfile(path);

            if (ofs == nullptr)
                throw except("sinker: ofstream pointer is null");

            ofs->close(); // Close it in advance, otherwise open will throw an exception.
            ofs->open(path, std::ios::out | std::ios::app);
        }

        void flush2disk(std::ofstream& ofs, const std::string& path)
        {
// #if defined (__linux__)
//             ofs.flush();

//             auto gnubuf = static_cast<__gnu_cxx::stdio_filebuf<char>*>(ofs.rdbuf());
//             if (!gnubuf)
//                 throw except("file_sinker::flush get stdio_filebuf error");

//             if (fsync(gnubuf->fd()) < 0)
//                 throw except("file_sinker::flush::fsync failed");
// #else
            open_logfile(&ofs, path); // Reopening the file means closing and then reopening the file,
                                      // which can refresh the system buffer.
// #endif
        }

    protected:
        formatter::ptr _fmtr;
    };

    class stdout_sinker : public sinker
    {
    public:
        stdout_sinker(bool colorful = config::instance().get_oncolor(),
                color_mode colmode  = config::instance().get_color(),
            formatter::ptr fmtr     = std::make_shared<formatter>())
            : _colorful(colorful)
            , _colmode(colmode)
            , sinker(fmtr)
        {}

        virtual void set_color(bool onclr) override { _colorful = onclr; }

        virtual void set_color_mode(color_mode mode) override { _colmode = mode; }

        virtual void sink(message&& msg) override
        {
            if (_colorful)
            {
                std::cout << util::color::onclr(_colmode[msg.levo])
                          << _fmtr->format(std::forward<message>(msg))
                          << util::color::offclr();
            }
            else
            {
                std::cout << _fmtr->format(std::forward<message>(msg));
            }
        }

        virtual void flush() override {}

        // useless
        virtual void set_logfile(const std::string& path) override
        {
            throw except("call set_logfile by stdout_sinker is not allow");
        }
        virtual void set_fn_patt(const std::string& patt) override
        {
            throw except("call set_fs_patt by stdout_sinker is not allow");
        }
        virtual void set_order(size_t n) override
        {
            throw except("call set_order by stdout_sinker is not allow");
        }
        virtual void set_max_file_size(size_enum size) override
        {
            throw except("call set_max_file_size by stdout_sinker is not allow");
        }
        virtual void set_max_live_time(time_enum time) override
        {
            throw except("call set_max_live_time by stdout_sinker is not allow");
        }
        virtual void set_max_history(size_t n) override
        {
            throw except("call set_max_history by stdout_sinker is not allow");
        }
        virtual void set_total_size_cap(size_enum size) override
        {
            throw except("call set_total_size_cap by stdout_sinker is not allow");
        }

    private:
        bool _colorful;
        color_mode _colmode;
    };

    class file_sinker : public sinker
    {
    public:
        file_sinker(const std::string& patt = config::instance().get_fn_pattern(),
                        formatter::ptr fmtr = std::make_shared<formatter>())
            : sinker(fmtr)
        {
            set_fn_patt(patt);
        }

        virtual void set_logfile(const std::string& path) override
        {
            // Delete old abandoned logfile that may exist
            if (!_fname.empty() && util::file::exists(_fname))
                util::file::remove(_fname);

            // open new logfile
            _fname = path;
            open_logfile(&_ofs, _fname);
        }

        virtual void set_fn_patt(const std::string& patt) override
        {
            // filename pattern is handed over to formatter,
            // to generate the actual filename.
            const auto& fname = formatter(patt).format(message(message::_4parse));
            set_logfile(fname);
        }

        // Format the message and output it to log file.
        virtual void sink(message&& msg) override
        {
            std::string data = _fmtr->format(std::forward<message>(msg));
            util::file::write(_ofs, data);
        }

        virtual void flush() override
        {
            sinker::flush2disk(_ofs, _fname);
        }

        // useless
        virtual void set_color(bool onclr) override
        {
            throw except("call set_color by file_sinker is not allow");
        }
        virtual void set_color_mode(color_mode mode) override
        {
            throw except("call set_color_mode by file_sinker is not allow");
        }
        virtual void set_order(size_t n) override
        {
            throw except("call set_order by file_sinker is not allow");
        }
        virtual void set_max_history(size_t n) override
        {
            throw except("call set_max_history by file_sinker is not allow");
        }
        virtual void set_max_file_size(size_enum size) override
        {
            throw except("call set_max_file_size by file_sinker is not allow");
        }
        virtual void set_max_live_time(time_enum time) override
        {
            throw except("call set_max_live_time by file_sinker is not allow");
        }
        virtual void set_total_size_cap(size_enum size) override
        {
            throw except("call set_total_size_cap by file_sinker is not allow");
        }

    protected:
        std::string _fname;   // actual filename
        std::ofstream _ofs;
    };

    // rolling is controlled by tow parameters,
    // max_file_size limits the size of each log file,
    // max_live_time limits the usage time of each log file,
    //
    // In addition, total_size_cap and max_history are not used to controll rolling,
    // only to decide whether to delete old files.
    class rolling_sinker : public sinker
    {
    public:
        rolling_sinker(const std::string& fn_patt   = config::instance().get_fn_pattern(),
                                   size_t history   = config::instance().get_max_history(),
                                   size_t file_size = config::instance().get_max_file_size(),
                                   size_t live_time = config::instance().get_max_live_time(),
                                   size_t size_cap  = config::instance().get_total_size_cap(),
                           formatter::ptr fmter     = std::make_shared<formatter>())
            : sinker(fmter)
            , _fname_patt(fn_patt)
            , _order(1)
            , _total_size(0)
            , _max_history(history)
            , _max_fsize(file_size)
            , _max_ltime(live_time)
            , _total_size_cap(size_cap)
        {}

        // The setting of filename pattern will take effect since the next rolling.
        virtual void set_fn_patt(const std::string& patt) override { _fname_patt = patt; }

        virtual void set_order(size_t n) override { _order = n; }

        virtual void set_max_history(size_t n) override { _max_history = n; }

        virtual void set_total_size_cap(size_enum size) override { _total_size_cap = size; }

        virtual void set_max_live_time(time_enum time) override { _max_ltime = time; }

        virtual void set_max_file_size(size_enum size) override { _max_fsize = size; }

    protected:
        virtual void roll() = 0;

        void toggle_logfile()
        {
            // Lock the toggle process to ensure thread safety.
            // (ps: it isn't necessary,
            //      because the entire output process is thread-safe with logger locking.)
            std::unique_lock<std::mutex> lock(_mtx);

            // open a new logfile
            std::string fname = formatter(_fname_patt, _order++).format(message(message::_4parse));
            _history_files.push_back(fname);

            open_logfile(&_ofs, fname);

            // no need to strictly check whether to delete old logfiles each sink,
            // it is sufficient to delete them when rolling.
            //
            // If there are too many log files or the total size is too large,
            // remove the oldest file.
            if (_history_files.size() > _max_history || _total_size >= _total_size_cap)
            {
                _total_size -= util::file::size(_history_files.front());

                util::file::remove(_history_files.front());
                _history_files.pop_front();
            }
        }

        virtual void flush() override
        {
            sinker::flush2disk(_ofs, _history_files.back());
        }

        // useless
        virtual void set_color(bool onclr) override
        {
            throw except("call set_color by rolling_sinker is not allow");
        }
        virtual void set_color_mode(color_mode mode) override
        {
            throw except("call set_color_mode by rolling_sinker is not allow");
        }
        virtual void set_logfile(const std::string& path) override
        {
            throw except("call set_logfile by rolling_sinker is not allow");
        }

    protected:
        std::string _fname_patt;
        std::ofstream _ofs;

        size_t _order;      // rolling file's sequence number record
        size_t _total_size; // current total size of all the log file

        size_t _max_fsize;      // limits the size of each log file
        size_t _max_ltime;      // limits the usage time of each log file,
        size_t _max_history;    // limits the count of log file
        size_t _total_size_cap; // limits the total size of all the log file

        std::list<std::string> _history_files; // history file record
        std::mutex _mtx; // lock when toggling logfile
    };

    class roll_over_time : public rolling_sinker
    {
    public:
        roll_over_time(const std::string& pattern   = config::instance().get_fn_pattern(),
                                   size_t history   = config::instance().get_max_history(),
                                   size_t live_time = config::instance().get_max_live_time(),
                                   size_t size_cap  = config::instance().get_total_size_cap(),
                           formatter::ptr fmter     = std::make_shared<formatter>())
            : rolling_sinker(pattern, history, 0, live_time, size_cap, fmter)
            , _cur_time(0) // Initialize cur_time to 0,
                           // ensuring the rolling function must be called to open the rolling file,
                           // before the first time sink.
        {}

        roll_over_time(size_t live_time)
            : rolling_sinker(config::instance().get_fn_pattern(),
                             config::instance().get_max_history(),
                             0,
                             live_time,
                             config::instance().get_total_size_cap())
            , _cur_time(0)
        {}

        // The usage time of log file is too long or
        // the total size of all log files is too large
        // Both will result in the log file being rolled over.
        virtual void roll() override
        {
            if (_cur_time + _max_ltime <= util::time::now())
            {
                toggle_logfile();
                _cur_time = util::time::now();
            }
        }

        // Before each time sink is rolling check.
        // If satisfied, we will roll the logfile then continue the output.
        virtual void sink(message&& msg) override
        {
            roll();

            std::string data = _fmtr->format(std::forward<message>(msg));
            util::file::write(_ofs, data);

            // Update the total size in a timely manner.
            _total_size += data.size();
        }

        // useless
        virtual void set_max_file_size(size_enum size) override
        {
            throw except("call set_max_file_size by roll_over_time is not allow");
        }

    private:
        time_t _cur_time;
    };

    class roll_over_size : public rolling_sinker
    {
    public:
        roll_over_size(const std::string& pattern   = config::instance().get_fn_pattern(),
                                   size_t history   = config::instance().get_max_history(),
                                   size_t file_size = config::instance().get_max_file_size(),
                                   size_t size_cap  = config::instance().get_total_size_cap(),
                           formatter::ptr fmter     = std::make_shared<formatter>())
            : rolling_sinker(pattern, history, file_size, 0, size_cap, fmter)
            , _cur_size(file_size) // Initialize cur_size to max_size,
                                   // ensuring the rolling function must be called to open the rolling file,
                                   // before the first time sink.
        {}

        roll_over_size(size_t file_size)
            : rolling_sinker(config::instance().get_fn_pattern(),
                             config::instance().get_max_history(),
                             file_size,
                             0,
                             config::instance().get_total_size_cap())
            , _cur_size(file_size)
        {}

        virtual void roll() override
        {
            if (_cur_size >= _max_fsize)
            {
                toggle_logfile();
                _cur_size = 0;
            }
        }

        virtual void sink(message&& msg) override
        {
            roll();

            std::string data = _fmtr->format(std::forward<message>(msg));
            util::file::write(_ofs, data);

            _cur_size += data.size();
            _total_size += data.size();
        }

        // useless
        virtual void set_max_live_time(time_enum time) override
        {
            throw except("call set_max_live_time by roll_over_size is not allow");
        }

    private:
        size_t _cur_size;
    };

    // Use factory mode to agent the creation of sinkers.
    struct sinker_factory
    {
        template<typename SinkType, typename... Args>
        static sinker::ptr create(Args&&... args)
        {
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
    };
}
